﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection.Emit;
using org.webguvenligi.NSecureImageLib.GraphicFileFormats.PNG.Compression;
using org.webguvenligi.NSecureImageLib.GraphicFileFormats.PNG.Exceptions;
using org.webguvenligi.NSecureImageLib.GraphicFileFormats.Utilities;

namespace org.webguvenligi.NSecureImageLib.GraphicFileFormats.PNG
{

    public sealed class PNGImage
    {
        private PNGImage(List<PNGBaseChunk> imageChunks,
                        List<NonFatalPNGException> nonFatalException,
                        List<PNGBaseChunk> invalidPngChunks)
        {
        }
    }

    public abstract class PNGBaseChunk
    {
        /// <summary>
        /// Gets the preciding types.
        /// </summary>
        /// <value>The preciding types.</value>
        protected virtual List<Type> PrecidingTypes
        {
            get
            {
                return null;
            }
        }
        private List<PNGBaseChunk> _PrecidingValues;
        /// <summary>
        /// Gets or sets the preciding values.
        /// </summary>
        /// <value>The preciding values.</value>
        protected List<PNGBaseChunk> PrecidingChunks
        {
            set
            {
                _PrecidingValues = value;
            }
            get
            {
                return _PrecidingValues;
            }
        }
        public const Int32 MaxChunkSize = 2147483647;

        public override string ToString()
        {
            return HelperUtilities.ToString(this);
        }
        protected abstract Type PrecidingType
        {
            get;
        }
        public delegate object PNGChunkConstructor(BinaryReader source);
        /// <summary>
        /// Bakes the constructor.
        /// </summary>
        /// <param name="sourceType">Type of the source.</param>
        /// <returns></returns>
        private static PNGChunkConstructor BakeConstructor(Type sourceType)
        {
            DynamicMethod dm = new DynamicMethod("MyCtor", sourceType, new Type[] { typeof(BinaryReader) },
                                                                                    typeof(PNGBaseChunk).Module);
            ILGenerator ilGenerator = dm.GetILGenerator();
            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.Emit(OpCodes.Newobj, sourceType.GetConstructor(new Type[] { typeof(BinaryReader) }));
            ilGenerator.Emit(OpCodes.Ret);
            return (PNGChunkConstructor)dm.CreateDelegate(typeof(PNGChunkConstructor));
        }
        /// <summary>
        /// Initializes the <see cref="PNGBaseChunk"/> class.
        /// </summary>
        static PNGBaseChunk()
        {
            _chunkTypeCache = new Dictionary<string, PNGChunkConstructor>();
            _chunkTypeCache.Add("IHDR", BakeConstructor(typeof(IHDRChunk)));
            _chunkTypeCache.Add("bKGD", BakeConstructor(typeof(BKGDChunk)));
            _chunkTypeCache.Add("PLTE", BakeConstructor(typeof(PLTEChunk)));
            _chunkTypeCache.Add("IDAT", BakeConstructor(typeof(IDATChunk)));
            _chunkTypeCache.Add("IEND", BakeConstructor(typeof(IENDChunk)));
            _chunkTypeCache.Add("tEXt", BakeConstructor(typeof(TextualChunk)));
            _chunkTypeCache.Add("zTXt", BakeConstructor(typeof(ZTextualChunk)));
            _chunkTypeCache.Add("tIME", BakeConstructor(typeof(TimeChunk)));
            _chunkTypeCache.Add("gAMA", BakeConstructor(typeof(GamaChunk)));
            _chunkTypeCache.Add("iCCP", BakeConstructor(typeof(ICCPChunk)));
            _chunkTypeCache.Add("pHYs", BakeConstructor(typeof(PhysChunk)));
            _chunkTypeCache.Add("sRGB", BakeConstructor(typeof(SRGBChunk)));
            _chunkTypeCache.Add("hIST", BakeConstructor(typeof(HistogramChunk)));
            _chunkTypeCache.Add("sBIT", BakeConstructor(typeof(SBitChunk)));
            _chunkTypeCache.Add("sPLT", BakeConstructor(typeof(SPLTChunk)));
            _chunkTypeCache.Add("cHRM", BakeConstructor(typeof(CHRMChunk)));
            _chunkTypeCache.Add("sCAL", BakeConstructor(typeof(ScalChunk)));
            _chunkTypeCache.Add("oFFs", BakeConstructor(typeof(OffChunk)));
            _chunkTypeCache.Add("pCAL", BakeConstructor(typeof(PCalChunk)));
            _chunkTypeCache.Add("tRNS", BakeConstructor(typeof(TrnsChunk)));
            _chunkTypeCache.Add("iTXt", BakeConstructor(typeof(ITextualChunk)));
            //ITextualChunk
        }

        private static Dictionary<string, PNGChunkConstructor> _chunkTypeCache;

        protected byte[] RawChunkData
        {
            get;
            set;
        }
        protected PNGBaseChunk PrecidingChunk
        {
            get;
            private set;
        }
        public int CurrentIndex
        {
            get;
            set;
        }
        public uint TotalChunkSize
        {
            get;
            set;
        }
        protected byte[] RawChunkType
        {
            get;
            set;
        }
        public ChunkTypeDetail ChunkTypeValue
        {
            get;
            protected set;
        }
        protected BinaryReader ChunkReader
        {
            get;
            set;
        }
        public uint CRCValue
        {
            get;
            private set;
        }
        protected BinaryReader PngReader
        {
            get;
            set;
        }
        protected PNGBaseChunk(BinaryReader pngReader)
        {
            this.PngReader = pngReader;
        }
        protected PNGBaseChunk()
        {
        }
        private void ProcessContents()
        {

            PngReader.EnsureBytes(TotalChunkSize);

            this.RawChunkData =
                   PngReader.ReadBytes((int)TotalChunkSize);

            PngReader.EnsureBytes(4);

            this.CRCValue = PngReader.ReadUInt32();

            if (CalculateCrcForChunkData() != this.CRCValue)
                throw new InvalidDataException("Crc Not Valid For This Chunk");

            MemoryStream chunkDataStream =
                new MemoryStream(this.RawChunkData, true);

            chunkDataStream.Position = 0;

            if (ChunkSize != -1 && TotalChunkSize != ChunkSize)
                throw new InvalidDataException("ChunkSize Not Valid For This Chunk");

            using (BinaryReader chunkReader =
                new BinaryReader(chunkDataStream))
            {
                ParseChunkData(chunkReader);
            }

        }

        protected virtual int ChunkSize
        {
            get
            {
                return -1;
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="chunkDataReader"></param>
        protected virtual void ParseChunkData(BinaryReader chunkDataReader)
        {
        }
        /// <summary>
        /// Writes the chunk data.
        /// </summary>
        /// <returns></returns>
        protected virtual byte[] WriteChunkData()
        {
            return RawChunkData;
        }
        /// <summary>
        /// Writes the chunk data.
        /// </summary>
        /// <param name="chunkWriter">The chunk writer.</param>
        public void WriteChunkData(BinaryWriter chunkWriter)
        {
            byte[] currentChunkData = WriteChunkData();
            chunkWriter.WriteLittleEndian(currentChunkData.Length);
            chunkWriter.Write(this.RawChunkType);
            chunkWriter.Write(currentChunkData);

            Crc32Hash tempHash = new Crc32Hash();
            tempHash.Update(this.RawChunkType);
            tempHash.Update(currentChunkData);

            chunkWriter.Write(tempHash.CrcByteArray);
        }
        /// <summary>
        /// Calculates the CRC for chunk data.
        /// </summary>
        /// <returns></returns>
        private uint CalculateCrcForChunkData()
        {
            byte[] crcArray =
                new byte[RawChunkData.Length + RawChunkType.Length];
            Array.Copy(RawChunkType, crcArray, RawChunkType.Length);
            Array.Copy(RawChunkData, 0, crcArray, RawChunkType.LongLength, RawChunkData.Length);
            return BitConverter.ToUInt32(Crc32.GetCrc(crcArray), 0);
        }
        /// <summary>
        /// Gets the chunks.
        /// </summary>
        /// <param name="pngReader">The PNG reader.</param>
        /// <returns></returns>
        public static IEnumerable<PNGBaseChunk> GetChunks(BinaryReader pngReader)
        {
            List<NonFatalPNGException> nonFatalExceptions =
                new List<NonFatalPNGException>();

            List<PNGBaseChunk> nonFatalCorruptChunks =
                new List<PNGBaseChunk>();

            List<PNGBaseChunk> pngChunkList =
                new List<PNGBaseChunk>();

            while (pngReader.BytesRemaining() > 0)
            {
                pngReader.EnsureBytes(4);
                uint TotalChunkSize =
                    pngReader.ReadLittleEndianUInt32();

                if (TotalChunkSize > PNGBaseChunk.MaxChunkSize)
                    throw new PngInvalidDataException("Max Chunk Length Exceeded. Possible Overflow Attempt");

                byte[] chunkTypeArray =
                    pngReader.ReadBytesEnsure(4);

                ChunkTypeDetail currentChunkType =
                    new ChunkTypeDetail(chunkTypeArray);

                PNGBaseChunk tempChunk;
                PNGChunkConstructor currentConstructor;
                if (_chunkTypeCache.TryGetValue(currentChunkType.ChunkTypeDescription, out currentConstructor))
                {
                    tempChunk = (PNGBaseChunk)currentConstructor(pngReader);
                }
                else
                {
                    if (currentChunkType.Ancillary)
                    {
                        tempChunk =
                            new PNG.UnknownAnchillaryChunk(pngReader)
                            {
                                ChunkTypeValue = currentChunkType
                            };
                    }
                    else
                    {
                        tempChunk =
                           new PNG.UnknownChunk(pngReader)
                           {
                               ChunkTypeValue = currentChunkType
                           };
                    }

                }

                Type detectedChunkType = tempChunk.GetType();
                tempChunk.ChunkTypeValue = currentChunkType;
                tempChunk.RawChunkType = chunkTypeArray;
                tempChunk.TotalChunkSize = TotalChunkSize;

                Type beforeChunk = tempChunk.PrecidingType;

                if (beforeChunk != null && pngChunkList.Count > 0)
                {
                    try
                    {
                        PNGBaseChunk beforeContainer =
                            pngChunkList.FindChunks(beforeChunk).Single() as PNGBaseChunk;
                        tempChunk.PrecidingChunk = beforeContainer;
                    }
                    catch
                    {
                        if (tempChunk.PrecidingChunk == null)
                            throw new PngInvalidDataException("Preciding Chunk For Given Type Is Missing.");
                    }
                }
                List<Type> precidingTypes = tempChunk.PrecidingTypes;
                if (pngChunkList.Count > 0 && precidingTypes != null && precidingTypes.Count > 0)
                {
                    List<PNGBaseChunk> targetChunks = new List<PNGBaseChunk>();
                    foreach (Type currentType in precidingTypes)
                    {
                        PNGBaseChunk foundChunk =
                            pngChunkList.FindChunks(currentType).SingleOrDefault() as PNGBaseChunk;
                        targetChunks.Add(foundChunk);
                    }
                    tempChunk.PrecidingChunks = targetChunks;
                }

                try
                {
                    tempChunk.ProcessContents();
                }
                catch (NonFatalPNGException nonFatalException)
                {
                    nonFatalExceptions.Add(nonFatalException);
                    nonFatalCorruptChunks.Add(tempChunk);
                }

                // A decoder may further verify that the next eight bytes contain an IHDR chunk header with the correct chunk length;
                // this will catch bad transfers that drop or alter null (zero) bytes. 


                if (pngChunkList.Count == 0)
                {
                    if (detectedChunkType != HEADERCHUNKTYPE)
                        throw new PngInvalidDataException("First Chunk Should Be Always IHDR Type");
                }
                else
                {
                    if (detectedChunkType == HEADERCHUNKTYPE)
                        throw new PngInvalidDataException("More then One IHDR Chunk");
                }
                tempChunk.PrecidingChunk = null;
                tempChunk.CurrentIndex = pngChunkList.Count;
                pngChunkList.Add(tempChunk);
                yield return tempChunk;
            }
            #region Chunk Validations
            ValidateChunkOrderAndContents(pngChunkList);
            #endregion
        }

        static Type HEADERCHUNKTYPE = typeof(IHDRChunk);

        private static IEnumerable<PNGBaseChunk> ChunkFinder(List<PNGBaseChunk> foundChunks,
                                                     string chunkType)
        {
            var textChunks = from p in foundChunks
                             where p.ChunkTypeValue.ChunkTypeDescription == chunkType
                             select p;
            return textChunks;
        }

        private static IEnumerable<PNGBaseChunk> ChunkFinder(List<PNGBaseChunk> foundChunks,
                                                     Type chunkType)
        {
            var textChunks = from p in foundChunks
                             where p.GetType().IsAssignableFrom(chunkType)
                             select p;
            return textChunks;
        }

        public static void ValidateChunkOrderAndContents(List<PNGBaseChunk> pngChunkList)
        {
            if (pngChunkList.Count == 0)
                throw new PNGBaseException("No Chunks Found");

            if (pngChunkList[pngChunkList.Count - 1].GetType() != typeof(IENDChunk))
                throw new IDATException("No IENDChunk Found Or Invalid Location For IENDChunk");

            int IHDRIndex = 0;
            IHDRChunk currentChunk = pngChunkList.FindChunk<IHDRChunk>(out IHDRIndex);
            if (IHDRIndex > 0)
                throw new IDATException("Header index wrong");
            if (IHDRIndex == -1)
                throw new IDATException("Header not located");

            string[] notMultipleAllowedChunks =
                new string[] { "IHDR", "PLTE", "IEND", "cHRM", "gAMA", "iCCP", "sBIT", "sRGB", "bKGD", "hIST", "tRNS", "pHYs", "tIME", "sCAL" };

            var chunkExtension =
                (from chunk in pngChunkList
                 where notMultipleAllowedChunks.Contains(chunk.ChunkTypeValue.ChunkTypeDescription)
                 group chunk by chunk.ChunkTypeValue.ChunkTypeDescription into g
                 select new { chunkData = g.Key, chunkMembers = g }).TakeWhile(n => n.chunkMembers.Count() > 1);

            if (chunkExtension.Count() > 0)
            {
                throw new MultipleChunkException(chunkExtension.First().chunkData,
                                                 chunkExtension.First().chunkMembers.Count(), 1);
            }

            var spltExtensions =
                (from chunk in pngChunkList
                 where (chunk.GetType() == typeof(SPLTChunk))
                 select ((SPLTChunk)chunk).Keyword);

            if (spltExtensions.Distinct().Count() != spltExtensions.Count())
                throw new PngInvalidDataException("Same Named Splt Extensions Found");

            var idatExtensions =
                (from chunk in pngChunkList
                 where chunk.ChunkTypeValue.ChunkTypeDescription == "IDAT"
                 group chunk by chunk.ChunkTypeValue.ChunkTypeDescription into g
                 select new { chunkData = g.Key, chunkMembers = g }).TakeWhile(n => n.chunkMembers.Count() > 0);

            if (idatExtensions.Count() == 0)
                throw new Exceptions.IDATException("No IDAT Chunks Found");
            else
            {
                foreach (var idat in idatExtensions)
                {
                    int beginIndex = idat.chunkMembers.First().CurrentIndex;
                    int endIndex = beginIndex + idat.chunkMembers.Count() - 1;
                    int totalElementCount = (from chunkMember in idat.chunkMembers
                                             where chunkMember.CurrentIndex >= beginIndex && chunkMember.CurrentIndex <= endIndex
                                             select chunkMember.CurrentIndex).Distinct().Count();
                    if ((totalElementCount != idat.chunkMembers.Count()) || (totalElementCount != ((endIndex - beginIndex) + 1)))
                        throw new Exceptions.IDATException("IDAT Chunks order invalid.");
                    break;
                }
            }
            ValidatePLTEChunk(pngChunkList, currentChunk);
            var idatChunks = from chunkMember in idatExtensions.First().chunkMembers
                             select chunkMember;
            //List<PNGBaseChunk> myList = idatChunks.ToList();
            // TODO Open Up Here Later
            //DecompressHuffmanStream(myList);          
        }

        /// <summary>
        /// Validates the PLTE chunk.
        /// </summary>
        /// <param name="pngChunkList">The PNG chunk list.</param>
        /// <param name="currentChunk">The current chunk.</param>
        private static void ValidatePLTEChunk(List<PNGBaseChunk> pngChunkList, IHDRChunk currentChunk)
        {
            //PLTE
            //This chunk shall appear for colour type 3, and may appear for colour types 2 and 6; it shall not appear for colour types 0 and 4. There shall not be more than one PLTE chunk.
            if (currentChunk.ColorType == IHDRChunk.ColorEnumType.PaletteIndex)
            {
                int PLTEIndex = 0;
                PLTEChunk currentPLTE = pngChunkList.FindChunk<PLTEChunk>(out PLTEIndex);
                if (currentPLTE == null)
                    throw new InvalidDataException("PLTE Chunk Missing");
            }

            if (pngChunkList.FindChunks<PLTEChunk>().Count() > 1)
                throw new InvalidDataException("Multiple PLTE Chunks Found");
        }

        /// <summary>
        /// Decompresses the huffman stream.
        /// </summary>
        /// <param name="myList">My list.</param>
        private static void DecompressHuffmanStream(List<PNGBaseChunk> myList)
        {
            IDATChunk tempChunk = (IDATChunk)myList[0];
            myList.RemoveAt(0);
            PngChunkReader tempReader = new PngChunkReader(myList);
            PngDecodingDataStream decoder =
                new PngDecodingDataStream(tempChunk, tempReader);
            int counter = 0;
            while (1 == 1)
            {
                try
                {
                    decoder.getNextDecodedByte();
                    ++counter;
                }
                catch
                {
                    System.Diagnostics.Debug.WriteLine(counter.ToString());
                }
            }
        }
    }
}